package com.practice.concurrency.lock;

import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

public class DemonstrateDeadLock {

    private static final int NUM_THREADS = 20;

    private static final int NUM_ACCOUNTS = 5;

    private static final int NUM_ITERATIONS = 1000000;

    public static void main(String[] args) throws InterruptedException {

        final Random random = new Random();
        final Account[] accounts = new Account[NUM_ACCOUNTS];

        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = new Account();
        }

        class TransferThread extends Thread {
            public void run() {
                for (int i = 0; i < NUM_ITERATIONS; i++) {
                    int fromAccountIndex = random.nextInt(NUM_ACCOUNTS);
                    int toAccountIndex = random.nextInt(NUM_ACCOUNTS);
                    int amount = random.nextInt(1000);

                    System.out.println(String.format("Thread %d round %d, transfer %d from %d to %d",
                            this.getId(), i, amount, fromAccountIndex, toAccountIndex));
                    transferMoney(accounts[fromAccountIndex], accounts[toAccountIndex], amount);
                }
            }
        }

        final Thread[] threads = new Thread[NUM_THREADS];
        for (int i = 0; i < NUM_THREADS; i++) {
            threads[i] = new TransferThread();
        }

        for (Thread thread : threads) {
            thread.start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println("Done");

    }

    private static void transferMoney(final Account fromAccount, final Account toAccount,
                                      int amount) {

        int fromHash = System.identityHashCode(fromAccount);
        int toHash = System.identityHashCode(toAccount);

        Runnable transfer = () -> {
            fromAccount.debit(amount);
            toAccount.debit(amount);
        };

        if (fromHash > toHash) {
            synchronized (fromAccount) {
                synchronized (toAccount) {
                    transfer.run();
                }
            }
        } else {
            synchronized (toAccount) {
                synchronized (fromAccount) {
                    transfer.run();
                }
            }
        }
    }

}


class Account {

    private AtomicInteger balance;


    public Account() {
        balance = new AtomicInteger(0);
    }

    public void credit(int amount) {
        balance.addAndGet(amount);
    }

    public void debit(int amount) {
        balance.addAndGet(-amount);
    }

}
